﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Web;
using Obra.Api;
using Obra.Common;
using System.Threading;
using System.ComponentModel.Design;

namespace Obra.SmugMug.Api
{
    public partial class MediaServiceProvider :
        ClientBase<ISmugMug>,
        IMediaService,
        IDisposable
    {
        private Obra.Api.ISession session;

        // WCF parameter that should be set in the config file.
        private const int MAX_RECEIVED_MESSAGE_SIZE = 102 * 1024 * 1024;

        private readonly WebClient webClient;

        private readonly ServiceContainer services;

        /// <summary>
        /// Static c-tor
        /// </summary>
        /// <remarks>
        /// <para>
        /// XXX: I should probably remove the public constructors, if I am 
        /// going to have a static constructor.  The public constructors are 
        /// used for testing.
        /// </para>
        /// </remarks>
        public static MediaServiceProvider Create()
        {
            WebHttpBinding webHttpBinding = new WebHttpBinding()
            {
                MaxReceivedMessageSize = MediaServiceProvider.MAX_RECEIVED_MESSAGE_SIZE,
            };

            return new MediaServiceProvider(
                webHttpBinding,
                new EndpointAddress(Strings.DefaultEndpoint));
        }

        /// <summary>
        /// Session information for SmugMug.
        /// </summary>
        public Obra.Api.ISession Session
        {
            get { return session; }
        }

        public MediaServiceProvider(
            Binding binding,
            EndpointAddress remoteAddress)
            : base(binding, remoteAddress)
        {
            this.webClient = new WebClient();
            this.services = new ServiceContainer();

            this.Endpoint.Behaviors.Add(
                new EndpointBehavior());

            this.Endpoint.Behaviors.Add(
                new WebHttpBehavior());
        }

        #region IDisposable Members

        void IDisposable.Dispose()
        {
            if (this.session != null)
            {
                this.LogOff();
            }
        }

        #endregion

        #region IMediaService Members

        /// <summary>
        /// Logon onto the SmugMug service.
        /// </summary>
        /// <param name="userName">
        /// The username to use when connecting.
        /// </param>
        /// <param name="password">
        /// The password to use when connecting.
        /// </param>
        public void LogOn(string userName, string password)
        {
            this.ClientCredentials.UserName.UserName = userName;
            this.ClientCredentials.UserName.Password = password;

            LoginWithPasswordContract contract = this.Channel.LoginWithPassword(
                userName,
                password,
                Strings.ApiKey);

            this.session = (Obra.Api.ISession)contract;
        }

        /// <summary>
        /// Logoff from SmugMug.
        /// </summary>
        public void LogOff()
        {
            this.Channel.Logout(
                this.Session.SessionId);
        }

        /// <summary>
        /// Create a new album.
        /// </summary>
        /// <param name="name">
        /// Name of the album.
        /// </param>
        /// <returns>
        /// Created album.
        /// </returns>
        public IAlbum CreateAlbum(
            string name)
        {
            this.Channel.CreateAlbum(
                this.Session.SessionId,
                name,
                0);

            IAlbum album = this.Channel.GetAlbums(
                this.Session.SessionId).Albums.Single(x => x.Name == name);

            this.albums.Add(album);
            return album;
        }

        /// <summary>
        /// Validate that the specified album exists.
        /// </summary>
        /// <param name="album">
        /// Album to validatate for existence.
        /// </param>
        /// <remarks>
        /// <para>
        /// Single validates that the specified album exists, and that there is
        /// only one Album with the specified name.  If not, an exception will
        /// be thrown.
        /// </para>
        /// </remarks>
        private void ValidateAlbumExists(IAlbum album)
        {
            this.Albums.Single(x => x.Name == album.Name);
        }

        /// <summary>
        /// Validate that the specified image exists.
        /// </summary>
        /// <param name="media">
        /// Media to validate for existence.
        /// </param>
        private void ValidateMediaExists(IMedia media)
        {
            this.Albums.SelectMany(x => x.Media).Single(x => x.Id == media.Id);
        }

        /// <summary>
        /// Delete the specified album.
        /// </summary>
        /// <param name="album">
        /// Album to delete.
        /// </param>
        public void DeleteAlbum(IAlbum album)
        {
            this.ValidateAlbumExists(album);
            
            this.Channel.DeleteAlbum(
                this.Session.SessionId,
                int.Parse(album.Id));
        }

        /// <summary>
        /// Delete the specified media file.
        /// </summary>
        /// <param name="album">
        /// Album containing the media item to delete.
        /// </param>
        /// <param name="media">
        /// Media item to delete.
        /// </param>
        public void DeleteMediaFile(IAlbum album, IMedia media)
        {
            this.ValidateAlbumExists(album);
            this.ValidateMediaExists(media);

            this.Channel.DeleteImage(
                this.Session.SessionId,
                int.Parse(media.Id));

            AlbumContract albumContract = (AlbumContract)this.Albums.Single(x => x.Name == album.Name);
            albumContract.ImageContractMap.Remove(media.Name);
        }

        private readonly List<IAlbum> albums =
            new List<IAlbum>();

        /// <summary>
        /// Get the albums for this account.
        /// </summary>
        public IList<IAlbum> Albums
        {
            get
            {
                if (this.albums.Count == 0)
                {
                    this.FetchAlbumsAndMedia();
                }
                return this.albums;
            }
        }

        /// <summary>
        /// Get the albums for this account.
        /// </summary>
        private void FetchAlbumsAndMedia()
        {
            foreach (var album in this.Channel.GetAlbums(this.Session.SessionId).Albums)
            {
                this.AddMediaToAlbum(album);
                this.albums.Add(album);
            }
        }

        /// <summary>
        /// Add the media that belongs to this album to this album.
        /// </summary>
        private void AddMediaToAlbum(
            AlbumContract album)
        {
            try
            {
                this.Channel.GetImages(this.Session.SessionId, album.Id).Images.ForEach(
                    x => album.ImageContractMap.Add(x.FileName, x));
            }
            catch (SmugMugException)
            {
                // Ignore, if the gallery is empty SmugMug returns an error.
            }
        }

        /// <summary>
        /// Upload a media file to the specified album.
        /// </summary>
        /// <param name="album">
        /// The album to upload the media to.
        /// </param>
        /// <param name="mediaName">
        /// The name of the media file to upload.
        /// </param>
        /// <param name="mediaStream">
        /// Stream containing the contents of the media file to upload.
        /// </param>
        public void UploadMediaFile(
            IAlbum album,
            string mediaName,
            Stream mediaStream)
        {
            IUploadService service = this.GetService<IUploadService>();
            service.UploadMediaFile(album, mediaName, mediaStream);
        }

        /// <summary>
        /// Download the specified media.
        /// </summary>
        /// <param name="media">
        /// The media to download.
        /// </param>
        /// <returns>
        /// Stream of the media's contents.
        /// </returns>
        public Stream DownloadMediaFile(
            IMedia media)
        {
            return this.webClient.OpenRead(media.Url);
        }

        #endregion

        #region IServiceContainer Members

        /// <summary>
        /// Adds the specified service to the service container, and optionally promotes the service to parent service containers.
        /// </summary>
        /// <param name="serviceType">The type of service to add.</param>
        /// <param name="callback">A callback object that is used to create the service. This allows a service to be declared as available, but delays the creation of the object until the service is requested.</param>
        /// <param name="promote">true to promote this request to any parent service containers; otherwise, false.</param>
        public void AddService(Type serviceType, ServiceCreatorCallback callback, bool promote)
        {
            this.services.AddService(serviceType, callback, promote);
        }

        /// <summary>
        /// Adds the specified service to the service container.
        /// </summary>
        /// <param name="serviceType">The type of service to add.</param>
        /// <param name="callback">A callback object that is used to create the service. This allows a service to be declared as available, but delays the creation of the object until the service is requested.</param>
        public void AddService(Type serviceType, ServiceCreatorCallback callback)
        {
            this.services.AddService(serviceType, callback);
        }

        /// <summary>
        /// Adds the specified service to the service container, and optionally promotes the service to any parent service containers.
        /// </summary>
        /// <param name="serviceType">The type of service to add.</param>
        /// <param name="serviceInstance">An instance of the service type to add. This object must implement or inherit from the type indicated by the <paramref name="serviceType"/> parameter.</param>
        /// <param name="promote">true to promote this request to any parent service containers; otherwise, false.</param>
        public void AddService(Type serviceType, object serviceInstance, bool promote)
        {
            this.services.AddService(serviceType, serviceInstance, promote);
        }

        /// <summary>
        /// Adds the specified service to the service container.
        /// </summary>
        /// <param name="serviceType">The type of service to add.</param>
        /// <param name="serviceInstance">An instance of the service type to add. This object must implement or inherit from the type indicated by the <paramref name="serviceType"/> parameter.</param>
        public void AddService(Type serviceType, object serviceInstance)
        {
            this.services.AddService(serviceType, serviceInstance);
        }

        /// <summary>
        /// Removes the specified service type from the service container, and optionally promotes the service to parent service containers.
        /// </summary>
        /// <param name="serviceType">The type of service to remove.</param>
        /// <param name="promote">true to promote this request to any parent service containers; otherwise, false.</param>
        public void RemoveService(Type serviceType, bool promote)
        {
            this.services.RemoveService(serviceType, promote);
        }

        /// <summary>
        /// Removes the specified service type from the service container.
        /// </summary>
        /// <param name="serviceType">The type of service to remove.</param>
        public void RemoveService(Type serviceType)
        {
            this.services.RemoveService(serviceType);
        }

        #endregion

        #region IServiceProvider Members

        /// <summary>
        /// Gets the service object of the specified type.
        /// </summary>
        /// <param name="serviceType">An object that specifies the type of service object to get.</param>
        /// <returns>
        /// A service object of type <paramref name="serviceType"/>.
        /// -or-
        /// null if there is no service object of type <paramref name="serviceType"/>.
        /// </returns>
        public object GetService(Type serviceType)
        {
            return this.services.GetService(serviceType);
        }

        public TService GetService<TService>()
        {
            return (TService)this.GetService(typeof(TService));
        }

        #endregion
    }
}
